package racer.client.gui.tiles;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;

import racer.client.gui.entities.Entity;
import racer.client.gui.ImageBank;


public class TiledMap {

	public static boolean debug = false;
	
	private final static int TILESIZE = ImageBank.getImage("hori").getHeight();
	private static int SCREEN_WIDTH_PIXELS = 1024;
	private static int SCREEN_HEIGHT_PIXELS = 900;
	
	// the map itself, as a two-dimensional array
	private Tile[][] tiles;
	// the available tiles for building a map
	private HashMap<String, Tile> availableTiles;
	private int map_width, map_height;
	private int screen_width_tiles = SCREEN_WIDTH_PIXELS/TILESIZE;
	private int screen_height_tiles = SCREEN_HEIGHT_PIXELS/TILESIZE +1;
	private Point2D.Double screenPosition = new Point2D.Double();
	
	private Shape finish = null;
	private int finishXTile, finishYTile;
	
	// temporary, for generating a random map
	private String[] tileids = {"hori","vert","b1","b2","b3","b4","vertifin","horifin"};
	
	public TiledMap(int screenWidth, int screenHeight) {
		SCREEN_WIDTH_PIXELS = screenWidth;
		SCREEN_HEIGHT_PIXELS = screenHeight;
		// default map width/height
		this.map_width = 20;
		this.map_height = 20;
		tiles = new Tile[map_width][map_height];
		availableTiles = new HashMap<String, Tile>();
		initAvailableTiles();
	}
	
	public TiledMap(int screenWidth, int screenHeight, int map_width, int map_height) {
		this.map_width = map_width;
		this.map_height = map_height;
		tiles = new Tile[map_width][map_height];
		availableTiles = new HashMap<String, Tile>();
		initAvailableTiles();
	}
	
	// initialize availableTiles with available tiles
	// information about tiles can maybe be stored in database
	private void initAvailableTiles() {
		Tile tile;
		tile = new Tile(ImageBank.getImage("hori"), "hori", false, false, true, true);
		availableTiles.put("hori", tile);
		tile = new Tile(ImageBank.getImage("vert"), "vert", true, true, false, false);
		availableTiles.put("vert", tile);
		tile = new Tile(ImageBank.getImage("b1"), "b1", false, true, true, false);
		availableTiles.put("b1", tile);
		tile = new Tile(ImageBank.getImage("b2"), "b2", false, true, false, true);
		availableTiles.put("b2", tile);
		tile = new Tile(ImageBank.getImage("b3"), "b3", true, false, false, true);
		availableTiles.put("b3", tile);
		tile = new Tile(ImageBank.getImage("b4"), "b4", true, false, true, false);
		availableTiles.put("b4", tile);
		tile = new Tile(ImageBank.getImage("vertifin"), "vertifin", true, true, false, false);
		availableTiles.put("vertifin", tile);
		tile = new Tile(ImageBank.getImage("horifin"), "horifin", false, false, true, true);
		availableTiles.put("horifin", tile);
		tile = new Tile(ImageBank.getImage("black"), "black", false, false, false, false);
		availableTiles.put("black", tile);
		tile = new Tile(ImageBank.getImage("empty"), "empty", true, true, true, true);
		availableTiles.put("empty", tile);
	}
	
	public void buildMap(int mapid) {
		// completely random map
		for(int i=0; i<map_height; i++) {
			for(int j=0; j<map_width; j++) {
				String tileid = randomTile();
				//tiles[j][i] = new Tile(ImageBank.getImage(tileid), j, i);
				tiles[j][i] = availableTiles.get(tileid);
				System.out.print(tileid + "\t");
			}
			System.out.println();
		}
		
	}
	private String randomTile() {		
		java.util.Random rand = new java.util.Random();
		int i = rand.nextInt(tileids.length);
		return tileids[i];
	}
	
	public void buildMap(File file) throws IOException {
		BufferedReader reader = new BufferedReader(new FileReader(file));
		ArrayList<String> lines = new ArrayList<String>();
		String line;
		int width=0;
		while( (line=reader.readLine()) != null) {
			if (!line.startsWith("#")) {
				lines.add(line);
				width = Math.max(width, line.length());
			}
		}
		int height = lines.size();
		this.map_height = height;
		this.map_width = width;
		tiles = new Tile[width][height];
		for(int i=0; i<height; i++) {
			line = lines.get(i);
			for(int j=0; j<line.length(); j++) {
				char ch = line.charAt(j);
				if (ch == '1') {
					tiles[j][i] = availableTiles.get("b1");
				}
				if (ch == '2') {
					tiles[j][i] = availableTiles.get("b2");
				}
				if (ch == '3') {
					tiles[j][i] = availableTiles.get("b3");
				}
				if (ch == '4') {
					tiles[j][i] = availableTiles.get("b4");
				}
				if (ch == 'h') {
					tiles[j][i] = availableTiles.get("hori");
				}
				if (ch == 'H') {
					tiles[j][i] = availableTiles.get("horifin");
					// 
					Polygon poly = new Polygon();
					poly.addPoint(j*TILESIZE + 45, i*TILESIZE + 2);
					poly.addPoint(j*TILESIZE + 45 + 13, i*TILESIZE + 2);
					poly.addPoint(j*TILESIZE + 45 + 13, i*TILESIZE + 2 + 96);
					poly.addPoint(j*TILESIZE + 45, i*TILESIZE + 2 + 96);
					finish = poly;
					finishXTile = j;
					finishYTile = i;
				}
				if (ch == 'v') {
					tiles[j][i] = availableTiles.get("vert");
				}
				if (ch == 'V') {
					tiles[j][i] = availableTiles.get("vertifin");
					// 
					Polygon poly = new Polygon();
					poly.addPoint(j*TILESIZE + 2, i*TILESIZE + 45);
					poly.addPoint(j*TILESIZE + 2 + 96, i*TILESIZE + 45);
					poly.addPoint(j*TILESIZE + 2 + 96, i*TILESIZE + 45 + 13);
					poly.addPoint(j*TILESIZE + 2, i*TILESIZE + 45 + 13);
					finish = poly;
					finishXTile = j;
					finishYTile = i;
				}
				if (ch == ' ') {
					tiles[j][i] = availableTiles.get("empty");
				}
			}
		}
		
	}
	
	// re-adjusts the entity's position if the current position conflicts with the map
	// concept: move entity horizontal, check collision; then move vertical and check collision again
	// if collision: move entity to border of tile
	public void collisionCheck(Entity ent) {
		boolean collLeft = false, collRight = false, collTop = false, collBottom = false;
		int tileLocX, tileLocY;
		Tile tile = getTile(ent.getX(), ent.getY());
		tileLocX = (int)(ent.getX()/TILESIZE);
		tileLocY = (int)(ent.getY()/TILESIZE);
		if (tile == null) {
			System.out.println("TiledMap.collisionCheck(): null tile");
			return;
		}
		
		double offset, entY, entX;
		// check collision with upper tile
		Point2D p2d = ent.getMostTopPosition();
		if(p2d!=null)
			entY = p2d.getY();
		else
			return;
		offset = tileLocY*TILESIZE - entY;
		if(offset > 0)
			collTop = true;
		if(collTop) {
			// re-adjust position
			if(!tile.up) {
				ent.setY(ent.getY() + offset);
			}
		}
		// check collision with lower tile		
		p2d = ent.getMostBottomPosition();
		if(p2d!=null)
			entY = p2d.getY();
		else
			return;
		offset = (tileLocY+1)*TILESIZE - entY;
		if(offset < 0)
			collBottom = true;
		if(collBottom) {
			// re-adjust position
			if(!tile.down) {
				ent.setY(ent.getY() + offset);
			}
		}
		// check collision with left tile		
		p2d = ent.getMostLeftPosition();
		if(p2d!=null)
			entX = p2d.getX();
		else
			return;
		offset = (tileLocX)*TILESIZE - entX;
		if(offset > 0)
			collLeft = true;
		if(collLeft) {
			// re-adjust position
			if(!tile.left) {
				ent.setX(ent.getX() + offset);
			}
		}
		// check collision with right tile		
		p2d = ent.getMostRightPosition();
		if(p2d!=null)
			entX = p2d.getX();
		else
			return;
		offset = (tileLocX+1)*TILESIZE - entX;
		if(offset < 0)
			collRight = true;
		if(collRight) {
			// re-adjust position
			if(!tile.right) {
				ent.setX(ent.getX() + offset);
			}
		}
	}
	
	// other way of checking collision, create Area objects of an entity and a tile
	// Area object of map consists of all the surrounding tiles of the current tile
	// and check intersection of both, which is also an Area object
	public Area collisionCheck2(Entity ent) {
		// x and y are the locations of the current tile (expressed in number of tiles)
		int x = (int)(ent.getX() / TILESIZE);
		int y = (int)(ent.getY() / TILESIZE);
		// create Area object containing the surrounding tiles, so without the tile the entity is standing on
		Area tileArea = new Area(new Rectangle( (x-1)*TILESIZE, (y-1)*TILESIZE , 3*TILESIZE, 3*TILESIZE));
		tileArea.subtract(new Area(new Rectangle(x*TILESIZE, y*TILESIZE, TILESIZE, TILESIZE)));
		
		Area entArea = new Area(ent.getShape());
		Area intersection = (Area)tileArea.clone();
		intersection.intersect(entArea);
		//return entArea;
		if(intersection.isEmpty())
			return null;
		else
			return intersection;			
	}
	
	public Area collisionWithFinishLine(Entity ent) {
		Area entArea = new Area(ent.getShape());
		Area finishArea = new Area(finish);
		finishArea.intersect(entArea);
		if(finishArea.isEmpty())
			return null;
		else
			return finishArea;
	}
	
	public boolean crossedFinish(Entity ent) {
		return (collisionWithFinishLine(ent) != null);
	}
	
	public Shape getFinish() {
		return finish;
	}
	
	public Tile getTile(double x, double y) {
		int ix = (int)(x/TILESIZE);
		int iy = (int)(y/TILESIZE);
		if (ix >= 0 && iy >= 0 && ix < map_width && iy < map_height)
			return tiles[(int)(x/TILESIZE)][(int)(y/TILESIZE)];
		else
			return null;
	}
	
	/**
	 *  A position can be relative to the map or relative to the screen
	 *  This method converts a screen position to a map position
	 */	
	public Point2D mapToScreenPosition(Point2D.Double p2d) {
		double x = p2d.getX() - (screenPosition.x - SCREEN_WIDTH_PIXELS/2);
		double y = p2d.getY() - (screenPosition.y - SCREEN_HEIGHT_PIXELS/2);
		return new Point2D.Double(x,y);
	}
	public AffineTransform mapToScreenTransform() {
    	// creating the AffineTransform instance 
		AffineTransform affineTransform = new AffineTransform();  
		// set the translation to the mid of the component 
		affineTransform.setToTranslation(- (screenPosition.x - SCREEN_WIDTH_PIXELS/2), - (screenPosition.y - SCREEN_HEIGHT_PIXELS/2));
		//affineTransform.setToTranslation(512,450);
		// rotate the image 
		//affineTransform.rotate(Math.toRadians(angle), entImage.getWidth()/2, entImage.getHeight()/2);
		return affineTransform;
	}
	
	/**
	 * Draws tiles to Graphics object g. It uses the screen size - in pixels - and the map size - in number of tiles
	 * @param g Graphics2D object to draw to
	 * @param x Horizontal position of the vehicle. Needed to know which part of the map to draw
	 * @param y Vertical position of the vehicle. Needed to know which part of the map to draw
	 */
	public void drawMap(Graphics g, Entity ent) {
		Graphics2D g2d = (Graphics2D)g;
		int iposx = (int)ent.getX();
		int iposy = (int)ent.getY();
		// offsetX/Y are the positions - relative to the screen - where the first tile should be drawn
		//TODO for some reason, if you draw the entity at position (0,0), it isn't drawn exactly in the
		// upper left corner; it is off by (-12, 50), hence this little cheat
		// tested it with another map, same problem; leave it like this for now
		//int offsetX = - iposx%TILESIZE;
		//int offsetY = - iposy%TILESIZE;
		int offsetX = - iposx%TILESIZE + 12;
		int offsetY = - iposy%TILESIZE - 50;
		//System.out.println(offsetX + "  " + offsetY);
		int firstTileX = (iposx - iposx % TILESIZE - (int)Math.floor(new Float(screen_width_tiles)/2 + 0.5f)*TILESIZE)/TILESIZE;
		int firstTileY = (iposy - iposy % TILESIZE - (int)Math.floor(new Float(screen_height_tiles)/2 + 0.5f)*TILESIZE)/TILESIZE;
		//System.out.println((iposx +"-"+ iposx % TILESIZE +"-"+ (int)Math.floor(new Float(screen_width)/2 + 0.5f) +"*"+ TILESIZE) +"/" +TILESIZE + "=" + offsetX);
		//System.out.println((iposy +"-"+ iposy % TILESIZE +"-"+ (int)Math.floor(new Float(screen_height)/2 + 0.5f) +"*"+ TILESIZE) +"/" +TILESIZE + "=" + offsetY);
		int tmpOffsetX = offsetX, tmpOffsetY = offsetY;
		for(int y=firstTileY; y<firstTileY+screen_height_tiles+1; y++) {
			tmpOffsetX = offsetX;
			for(int x=firstTileX; x<firstTileX+screen_width_tiles+2; x++) {
				// respect array boundaries
				if(x >= 0 && x < map_width && y >= 0 && y < map_height) {
					Tile t = tiles[x][y];
					if(t != null) {
						BufferedImage tile = t.getImage();
						if (tile != null) {
							g2d.drawImage(tile, tmpOffsetX , tmpOffsetY , null);
						}
					}
				} else {
					// when the player is located near the edges of the map and a part of "no man's land" has to be drawn
					g2d.drawImage(ImageBank.getImage("black"), tmpOffsetX, tmpOffsetY, null);
				}
				tmpOffsetX+=TILESIZE;
				if(debug) {
					g2d.setColor(Color.BLACK);
					g2d.drawLine(tmpOffsetX, 0, tmpOffsetX, SCREEN_HEIGHT_PIXELS);
				}
			} // end width loop
			if(debug) {
				g2d.setColor(Color.BLACK);
				g2d.drawLine(0, tmpOffsetY, SCREEN_WIDTH_PIXELS, tmpOffsetY);
			}
			tmpOffsetY+=TILESIZE;
		} // end height loop
		screenPosition.x = ent.getX();
		screenPosition.y = ent.getY();
	}	
}
